#include "execute.h"

#include <algorithm> // std::find std::find_if std::for_each std::fill
#include <utility> // std::swap
#include <tuple> // std::tuple std::tuple_size std::tuple_element std::apply
#include <type_traits> // std::integral_constant std::common_type_t
#include <cstdlib> // std::size_t std::abort std::nullptr_t
#include <array> // std::array
#include <iterator> // std::iterator_traits

#include <sys/wait.h> // waitpid
#include <unistd.h> // close pipe dup2 fork execvp read write pid_t
#include <fcntl.h> // fcntl F_SETFD FD_CLOEXEC
#include <errno.h> // errno E*

#include "simple/support/range.hpp" // support::range
#include "simple/support/meta/pend.hpp" // support::meta::concat_t
#include "simple/support/enum.hpp" // support::to_integer
#include "simple/support/tuple_utils.hpp" // support::apply_for support::define_tuple_operators support::trivial_tuple_accessor simple::support::operator_flag
#include "simple/support/vuple.hpp" // support::vuple
#include "simple/support/algorithm/find.hpp" // support::find_if

#include "standard.h" // all_standard_handles
#include "handle.h" // handle operation
#include "handle_common.hpp" // custom_deleter


// TODO: assert all unchecked errors
namespace simple::io
{

	template <typename F>
	auto flatten_arg(F f) { return [f](auto&& t){ return std::apply(f,t); }; }

	process_id wait_process_config::null() noexcept
	{ return process_id{-1}; }

	void wait_process_config::deleter(process_id id) noexcept
	{ int result; ::waitpid(static_cast<pid_t>(support::to_integer(id)), &result, 0); }

	template class custom_deleter<wait_process_config>;

	execute_result execute(std::array<int,exec_capture_max> captures, const char* file, const char** args) noexcept
	{
		// TODO: assert captures has no repeats, and only trailing -1s
		auto captures_end = std::find(captures.begin(), captures.end(), -1);

		std::array<std::array<int,2>,exec_capture_max> pipes;
		auto pipes_end = pipes.begin() + (captures_end - captures.begin());
		if(std::find_if(pipes.begin(), pipes_end, [](auto& pipe_handles)
		{
			return ::pipe(pipe_handles.data()) == -1;
		}) != pipes_end)
		{
			switch(errno)
			{
				case EMFILE: return process_descriptor_limit_reached{errno};
				case ENFILE: return system_descriptor_limit_reached{errno};
				default: return error{errno};
			}
		}

		std::fill(pipes_end, pipes.end(), std::array{-1,-1});

		constexpr unsigned parent_side = 0;
		constexpr unsigned child_side = 1;
		support::range capture_pipes {
			support::vuple{captures.begin(), pipes.begin()},
			support::vuple{captures_end, pipes_end}
		};

		for(auto&& [capture, pipe] : capture_pipes)
		{
			support::apply_for(capture, [&pipe](auto standard_handle) {
				if constexpr (interface_for_standard_redirect_v<decltype(standard_handle)> == operation::write)
					std::swap(pipe[parent_side], pipe[child_side]);
			}, all_standard_handles);
		}

		std::array child_error_pipe{-1, -1};
		if(::pipe(child_error_pipe.data()) == -1)
		{
			switch(errno)
			{
				case EMFILE: return process_descriptor_limit_reached{errno};
				case ENFILE: return system_descriptor_limit_reached{errno};
				default: return error{errno};
			}
		}

		auto pid = ::fork();
		if(pid == -1)
		{
			switch(errno)
			{
				case EAGAIN: return process_limit_reached{errno};
				// case ENNOMEM:  not sure...
				default: return error{errno};
			}
		}

		if(pid == 0)
		{
			std::for_each(pipes.begin(), pipes_end, [](auto p) { ::close(p[parent_side]); });
			::close(child_error_pipe[parent_side]);

			if
			(
				support::find_if(capture_pipes.begin(), capture_pipes.end(), flatten_arg([](auto&& capture, auto&& pipe)
					{ return ::dup2(pipe[child_side], capture) == -1; })) != capture_pipes.end() ||
				::fcntl(child_error_pipe[child_side], F_SETFD, FD_CLOEXEC) == -1 ||
				::execvp(file, const_cast<char*const*>(args)) == -1
			)
			{
				auto error = errno;
				::write(child_error_pipe[child_side], &error, sizeof(error));
				::close(child_error_pipe[child_side]);
				std::for_each(pipes.begin(), pipes_end, [](auto p) { ::close(p[child_side]); });
				std::abort();
			}
		}

		std::for_each(pipes.begin(), pipes_end, [](auto p) { ::close(p[child_side]); });
		::close(child_error_pipe[child_side]);

		auto child_error = errno; // init just to deduce the type
		auto error_read = ::read(child_error_pipe[parent_side], &child_error, sizeof(errno));
		::close(child_error_pipe[parent_side]);

		if(error_read == -1)
		{
			switch(errno)
			{
				// TODO: child lost or something? check status with waitpid here, since everything before exec is our responsibility? on the other hand if process is killed or something, who cares if it was before or after exec?
				default: return error{errno};
			}
		}

		if(error_read == sizeof(child_error))
		{
			switch(child_error)
			{
				case E2BIG: return arguments_size_limit_reached{errno};
				case ENOMEM: return executable_size_limit_reached{errno};
				case EINVAL: return executable_format_not_supported{errno};
				// case ENOEXEC: return executable_format_unrecognized{errno}; // canpt happen with execvp specifically, and we use that for automatic PATH search
				case EACCES: return access_denied{errno};
				case ELOOP: return symbolic_link_loop{errno};
				case ENAMETOOLONG: return path_component_length_exceeded{errno};
				case ENOENT: return no_such_entity{errno};
				case ENOTDIR: return no_such_directory{errno};
				case ETXTBSY: return file_is_being_written{errno};
				default: return error{child_error};
			}
		}

		return execute_success_t{
			process_id{pid},
			handle{pipes[0][parent_side]},
			handle{pipes[1][parent_side]},
			handle{pipes[2][parent_side]}
		};
	}

} // namespace simple::io
